import { inject, onUnmounted, reactive, shallowReadonly, UnwrapRef } from 'vue';
import { INJECT_KEY, AxiosVue3Config } from './axiosVue3Plugin';
import Axios, { AxiosInstance, AxiosResponse, AxiosRequestConfig, CancelTokenSource, CancelToken } from 'axios';


export declare interface AxiosVue3Instance<T> {
  /**
   * Whether there is request in process.
   * 
   * This field is reactive.
   */
  readonly loading: boolean;
  /**
   * When request success, will assign res.data to this.
   * 
   * This field is reactive.
   */
  readonly data: UnwrapRef<T> | T | undefined;
  /**
   * When request failed, the error message will assigned to this.
   * 
   * This field is reactive.
   */
  readonly error: any;
  /**
   * Cancel the running request. It is safe to call this in case there is no running request.
   */
  readonly cancel: (msg?: string) => AxiosVue3Instance<T>;
  /**
   * By default, we use axios's default instance, so any Interceptor on it will still work.
   * 
   * In case you don't use the default instance, please use this API to set it.
   */
  readonly axiosInstance: (axiosInstance: AxiosInstance) => AxiosVue3Instance<T>;
  /**
   * Config auto retry, when request failed, retry it up to `maxCount` times.
   * Also you can config the `interval`, unit: millisecond
   */
  readonly autoRetry: (enabled?: boolean, maxCount?: number, interval?: number) => AxiosVue3Instance<T>;
  /**
   * Whether cancel request when the component which called this hook is unmounted.
   */
  readonly cancelOnUnmounted: (enabled?: boolean) => AxiosVue3Instance<T>;
  /**
   * Same API with axios.request
   */
  readonly request: <Type = T, R = AxiosResponse<Type>> (config: AxiosRequestConfig) => Promise<R>;
  /**
   * Same API with axios.get
   */
  readonly get: <Type = T, R = AxiosResponse<Type>>(url: string, config?: AxiosRequestConfig) => Promise<R>;
  /**
   * Same API with axios.delete
   */
  readonly delete: <Type = T, R = AxiosResponse<Type>>(url: string, config?: AxiosRequestConfig) => Promise<R>;
  /**
   * Same API with axios.head
   */
  readonly head: <Type = T, R = AxiosResponse<Type>>(url: string, config?: AxiosRequestConfig) => Promise<R>;
  /**
   * Same API with axios.options
   */
  readonly options: <Type = T, R = AxiosResponse<Type>>(url: string, config?: AxiosRequestConfig) => Promise<R>;
  /**
   * Same API with axios.post
   */
  readonly post: <Type = T, R = AxiosResponse<Type>>(url: string, data?: any, config?: AxiosRequestConfig) => Promise<R>;
  /**
   * Same API with axios.put
   */
  readonly put: <Type = T, R = AxiosResponse<Type>>(url: string, data?: any, config?: AxiosRequestConfig) => Promise<R>;
  /**
   * Same API with axios.patch
   */
  readonly patch: <Type = T, R = AxiosResponse<Type>>(url: string, data?: any, config?: AxiosRequestConfig) => Promise<R>;
}

type Mutable<T> = { -readonly [P in keyof T]: T[P]; }

type AxiosVue3InstanceMutable<T> = Mutable<Omit<AxiosVue3Instance<T>, "data">> & { data: T | undefined }

type RequestArgsOne = {
  type: 'request';
  config: AxiosRequestConfig;
}

type RequestArgsTwo = {
  type: 'get' | 'delete' | 'head' | 'options';
  url: string;
  config?: AxiosRequestConfig | undefined;
}

type RequestArgsThree = {
  type: 'post' | 'put' | 'patch';
  url: string;
  data?: any,
  config?: AxiosRequestConfig | undefined;
}

export type RequestArgs = RequestArgsOne | RequestArgsTwo | RequestArgsThree;

export const withCancelToken = (args: RequestArgs, cancelToken: CancelToken): RequestArgs => {
  return {
    ...args,
    config: {
      ...args.config,
      cancelToken
    }
  }
}

export const makeRequest = <T>(instance: AxiosInstance, args: RequestArgs): Promise<T> => {
  switch (args.type) {
    case 'request':
      return instance[args.type]<any, T>(args.config);
    case 'get':
    case 'delete':
    case 'head':
    case 'options':
      return instance[args.type]<any, T>(args.url, args.config);
    case 'post':
    case 'put':
    case 'patch':
      return instance[args.type]<any, T>(args.url, args.data, args.config);
    default:
      return args;
  }
}

export const init = <T>(options: AxiosVue3Config) => {
  const globalOptions: AxiosVue3Config = inject(INJECT_KEY) || {};
  let autoRetryConfig = options.autoRetry ?? globalOptions.autoRetry ?? false;
  if (typeof autoRetryConfig === 'boolean') {
    autoRetryConfig = { enabled: autoRetryConfig }
  }
  const config = {
    axiosInstance: options.axiosInstance || globalOptions.axiosInstance || Axios,
    autoRetry: {
      enabled: autoRetryConfig.enabled,
      maxCount: autoRetryConfig.maxCount ?? 1,
      interval: autoRetryConfig.interval ?? 1000,
    },
    cancelOnUnmounted: options.cancelOnUnmounted ?? options.cancelOnUnmounted ?? false,
  }
  const state = {
    // don't read axios.loading inside request, to avoid trigger watchEffect when axios.loading changed.
    loading: false,
    // cancelToken source
    source: undefined as CancelTokenSource | undefined,
    // used for retry
    timer: undefined as number | undefined,
    // assign each request an uniqueId, and record the latest one.
    latestId: 0,
  }
  const handler = {
    run: undefined as unknown as <R>(args: RequestArgs) => Promise<R>
  }
  const axios: AxiosVue3InstanceMutable<T> = reactive({
    loading: false,
    // latest response.data
    data: undefined,
    // latest catched error
    error: undefined,
    cancel: (msg) => {
      clearTimeout(state.timer);
      state.source?.cancel(msg);
      return axiosReadonly;
    },
    autoRetry: (enabled, maxCount, interval) => {
      config.autoRetry.enabled = enabled ?? true;
      maxCount && (config.autoRetry.maxCount = maxCount);
      interval && (config.autoRetry.interval = interval);
      return axiosReadonly;
    },
    axiosInstance: (axiosInstance) => {
      config.axiosInstance = axiosInstance;
      return axiosReadonly;
    },
    cancelOnUnmounted: (enable) => {
      config.cancelOnUnmounted = enable ?? true;
      return axiosReadonly;
    },
    request: <Type = T, R = AxiosResponse<Type>>(config: AxiosRequestConfig): Promise<R> => handler.run<R>({ type: 'request', config }),
    get: <Type = T, R = AxiosResponse<Type>>(url: string, config?: AxiosRequestConfig): Promise<R> => handler.run<R>({ type: 'get', url, config }),
    delete: <Type = T, R = AxiosResponse<Type>>(url: string, config?: AxiosRequestConfig): Promise<R> => handler.run<R>({ type: 'delete', url, config }),
    head: <Type = T, R = AxiosResponse<Type>>(url: string, config?: AxiosRequestConfig): Promise<R> => handler.run<R>({ type: 'head', url, config }),
    options: <Type = T, R = AxiosResponse<Type>>(url: string, config?: AxiosRequestConfig): Promise<R> => handler.run<R>({ type: 'options', url, config }),
    post: <Type = T, R = AxiosResponse<Type>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R> => handler.run<R>({ type: 'post', url, data, config }),
    put: <Type = T, R = AxiosResponse<Type>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R> => handler.run<R>({ type: 'put', url, data, config }),
    patch: <Type = T, R = AxiosResponse<Type>>(url: string, data?: any, config?: AxiosRequestConfig): Promise<R> => handler.run<R>({ type: 'patch', url, data, config }),
  });
  const axiosReadonly: AxiosVue3Instance<T> = shallowReadonly(axios);

  onUnmounted(() => {
    if (config.cancelOnUnmounted) {
      axios.cancel();
    }
  })

  return { config, state, axios, axiosReadonly, handler };
}
